package org.dei.perla.sys.registry;

import java.util.Comparator;

import org.dei.perla.parser.expressions.Constant;
import org.dei.perla.parser.expressions.ConstantCastException;
import org.dei.perla.parser.expressions.ConstantFloat;
import org.dei.perla.parser.expressions.ConstantID;
import org.dei.perla.parser.expressions.ConstantInteger;
import org.dei.perla.parser.expressions.ConstantLogic;
import org.dei.perla.parser.expressions.ConstantString;
import org.dei.perla.parser.expressions.ConstantTimestamp;
import org.dei.perla.parser.expressions.LogicValue;
import org.dei.perla.parser.expressions.OperationNotSupportedException;

public class ConstantComparator implements Comparator<Constant> {
	
	// Istanza comparatore
	private static ConstantComparator instance = null;
	
	/**
	 * Costruttore privato per l'utilizzo esclusivo tramite getInstance()
	 */
	private ConstantComparator() { }
	
	/**
	 * Metodo da utilizzare per ottenere un'istanza del comparatore
	 * @return istanza del comparatore
	 */
	public static final ConstantComparator getInstance() {
		if (instance == null) {
			instance = new ConstantComparator();
		}
		return instance;
	}

	/**
	 * Esegue il confronto tra due ConstantBuiltin
	 * Se due costanti sono di tipo diverso (non confrontabile) si applica 
	 * questa regola:
	 * [Float, Integer] > [String, ID] > Timestamp > Logic > Null
	 * 
	 * ConstantFloat e ConstantInteger, essendo tipi numerici,
	 * possono essere confrontati senza grossi problemi tra di loro.
	 * Lo stesso discorso vale per ConstantString e ConstantID
	 */
	public int compare(Constant o1, Constant o2) {
		
		Class<? extends Constant> o1Class = o1.getClass();
		Class<? extends Constant> o2Class = o2.getClass();
		
		try {
			
			// Stessa classe, utilizziamo i metodi propri delle Constant
			// per eseguire il confronto
			if (o1Class == o2Class) {
				
				if (o1Class.equals(ConstantLogic.class)) {
					// Nel caso di ConstantLogic devo utilizzare delle
					// regole diverse da quelle di default
					return compareLogic(o1.getValueLogic(), o2.getValueLogic());
				} else if (o1Class.equals(ConstantID.class)) {
					// Nel caso di ConstantID devo utilizzare un metodo diverso
					// da quello embedded nella costante, vista la mancanza di 
					// isGreater
					return o1.getValueString().compareTo(o2.getValueString());
				} else {
					// Uso i medoti di default delle classi per 
					// decidere il vincitore del confronto
					if (isTrue(o1.isEqual(o2))) {
						return 0;
					} else if (isTrue(o1.isGreater(o2))) {
						return 1;
					} else {
						return -1;
					}
				}
					
			} else {
				
				// o1 appartiene al gruppo di classi numeriche
				if (isNumeric(o1Class)) {
					return compareWithNumeric(o1.getValueFloat(), o2);
					
				// o1 appartiene al gruppo di classi stringa
				} else if (isString(o1Class)) {
					return compareWithString(o1.getValueString(), o2);
					
				// o1 ha classe ConstantTimestamp
				} else {
					return compareLowestPriorityClasses(o1Class, o2Class);
				}
			}
			
		} catch (OperationNotSupportedException e) {
			return 0;
		} catch (ConstantCastException e) {
			return 0;
		}
	}
	
	/**
	 * Indica se il valore ConstantLogic e' pari a' LogicValue.TRUE
	 * @param value
	 * @return true (boolean) nel caso la costante true, false altrimenti
	 */
	private static final boolean isTrue(ConstantLogic value) {
		return value.getValueLogic() == LogicValue.TRUE;
	}
	
	/**
	 * Metodo per il controllo dell'appartenenza di una costante al gruppo
	 * di tipi numerici [ConstantInteger, ConstantFloat]
	 * @param constant sottoclasse di ConstantBuiltin da controllare
	 * @return true nel caso la costante appartenga al gruppo, false altrimenti
	 */
	private static final Boolean isNumeric(Class<? extends Constant> constantClass) {
		if (constantClass.equals(ConstantInteger.class) || 
				constantClass.equals(ConstantFloat.class)) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Metodo per il controllo dell'appartenenza di una costante al gruppo
	 * di tipi stringa [ConstantInteger, ConstantFloat]
	 * @param constantClass sottoclasse di ConstantBuiltin da controllare
	 * @return true nel caso la costante appartenga al gruppo, falsea altrimenti
	 */
	private static final Boolean isString(Class<? extends Constant> constantClass) {
		if (constantClass.equals(ConstantString.class) || 
				constantClass.equals(ConstantID.class)) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Confronto fra un oggetto di tipo numerico ed un oggetto sottoclasse di
	 * ConstantBuiltin
	 * @param firstValue primo elemento del confronto 
	 * @param secondValue secondo elemento del confronto
	 * @return 0 se firstValue == secondValue, 1 se firstValue > secondValue,
	 * -1 se firstValue < secondValue
	 */
	private static final int compareWithNumeric(Double firstValue, Constant secondValue) {
		
		if (isNumeric(secondValue.getClass())) {
			// Nel caso anche other appartenga al gruppo di costanti numeriche
			// confronto direttamente il valore delle costanti
			Double secondNumericValue = Double.valueOf(0);
			try {
				secondNumericValue = secondValue.getValueFloat();
			} catch (ConstantCastException e) {
				// Visto il controllo eseguito nell'if non potremo mai
				// finire in questo catch
			}
			return firstValue.compareTo(secondNumericValue);
		} else {
			// Nel caso le 2 costanti non appartengano allo stesso gruppo
			// ritorno 1, visto che le costanti numeriche sono per definizione
			// maggiori di qualsiasi altro tipo di costante
			return 1;
		}
	}
	
	/**
	 * Confronto fra un oggetto di tipo stringa ed un oggetto sottoclasse di
	 * ConstantBuiltin
	 * @param firstValue primo elemento del confronto 
	 * @param secondValue secondo elemento del confronto
	 * @return 0 se firstValue == secondValue, 1 se firstValue > secondValue,
	 * -1 se firstValue < secondValue
	 */
	private static final int compareWithString(String firstValue, Constant secondValue) {
		
		if (isNumeric(secondValue.getClass())) {
			// Nel caso secondValue sia di tipo numerico ritorno -1,
			// visto che per definizione le costanti numeriche sono
			// sempre maggiori delle costanti stringa
			return -1;
		} else if (isString(secondValue.getClass())) {
			String secondStringValue = "";
			try {
				secondStringValue = secondValue.getValueString();
			} catch (ConstantCastException e) {
				// Visto il controllo eseguito nell'if non potremo mai
				// finire in questo catch
			}
			return firstValue.compareTo(secondStringValue);
		} else {
			// Nel caso le 2 costanti non appartengano allo stesso gruppo
			// ritorno 1, visto che le costanti stringa sono per definizione
			// maggiori di qualsiasi altro tipo di costante (eccetto le
			// numeriche, che sono state gia' scartate come opzione)
			return 1;
		}
	}

	/**
	 * Confronta le 2 classi per decidere quale dovra' "vincere"
	 * il confronto 
	 * @param firstValue prima classe da confrontare
	 * @param secondValue seconda classe da confrontare
	 * @return -1 se firstValue < secondValue, 1 se firstValue > secondValue
	 */
	private static final int compareLowestPriorityClasses(Class<? extends Constant> firstValue, Class<? extends Constant> secondValue) {
		if (firstValue.equals(ConstantTimestamp.class)) {
			// firstValue e' di classe ConstantTimestamp
			if (isNumeric(secondValue) || isString(secondValue)) {
				// Se secondValue ha priorita' maggiore firstValue perde
				return -1;
			} else {
				// Se secondValue ha priorita' minore firstValue vince
				return 1;
			}
		} else if (firstValue.equals(ConstantLogic.class)) {
			// firstValue e' di classe ConstantLogic
			if (isNumeric(secondValue) || isString(secondValue) || 
					secondValue.equals(ConstantTimestamp.class)) {
				// Se secondValue ha priorita' maggiore firstValue perde
				return -1;
			} else {
				// Se secondValue ha priorita' minore firstValue vince
				return 1;
			}
		} else {
			// firstValue e' di classe ConstantNull, perde sempre con
			// tutte le altre classi
			return -1;
		}
	}
	
	/**
	 * Esegue il confronto tra 2 LogicValue. Non si utilizzano i metodi di
	 * default di ConstantLogic per i seguenti motivi
	 * - ConstantLogic non ha alcun metodo per l'ordinamento
	 * - ConstantLogic, nel caso UNKNOWN == UNKNOWN restituisce UNKNOWN (corretto,
	 * ma non utilizzabile nel caso si necessiti di un ordinamento totale delle costanti) 
	 * @param firstValue primo LogicValue da confrontare
	 * @param secondValue secondo LogicValue da confrontare
	 * @return 1 se true > false || true > unknown || false > unknown, 0 se true == true || false == false
	 * || unknown == unknown, -1 se false > true, unknown > false, unknown > true
	 */
	private static final int compareLogic(LogicValue firstValue, LogicValue secondValue) {
		if (firstValue == LogicValue.TRUE) {
			if (secondValue == LogicValue.TRUE) {
				return 0;
			} else {
				return 1; // secondValue == LogicValue.UNKNOWN || secondValue == LogicValue.FALSE
			}
		} else if (firstValue == LogicValue.FALSE) {
			if (secondValue == LogicValue.FALSE) {
				return 0;
			} else if (secondValue == LogicValue.TRUE) {
				return -1;
			} else {
				return 1; // secondValue == LogicValue.UNKNOWN
			}
		} else {
			// firstValue == LogicValue.UNKNOWN
			if (secondValue == LogicValue.UNKNOWN) {
				return 0;
			} else {
				return -1; // secondValue == LogicValue.TRUE || secondValue == LogicValue.FALSE
			}
		}
	}
}
